package ceura.ia.practica;

import ceura.ia.practica.heuristica.HeuristicaStrategy;
import ceura.ia.practica.listener.AStarListener;
import java.util.ArrayList;

/**
 * Esta clase representa un calculador del camino más corto dado un laberinto
 * que contiene la situacion inicial y final así como aquellos puntos del
 * laberinto por los que no se puede pasar. Se ha implementado el algoritmo A*
 * para realizar la busqueda de dicho laberinto.
 *
 * Para obtener los resultados de dicho calculo debemos subscribirnos al sistema
 * de listeners y escuchar los cambios de cada iteración del algoritmo los
 * cuales serán notificados al final de la iteración y cuando el algoritmo
 * termina.
 *
 * @author Jorge Madrid y Sergio Redondo.
 */
public class AlgoritmoAStar {

    /**
     * Coste de moverse en dirección recto.
     */
    public static final int RECTO = 10,
            /**
             * Coste de moverse en dirección diagonal.
             */
            DIAGONAL = 14;
    private HeuristicaStrategy heuristica;
    private ArrayList<AStarListener> listeners;
    private long sleepTime;

    /**
     *
     * @param heuristica Heurística que aplicara el algorítmo para decidir el
     * mejor camino.
     */
    public AlgoritmoAStar(HeuristicaStrategy heuristica) {
        this.heuristica = heuristica;
        listeners = new ArrayList<AStarListener>();
    }

    /**
     *
     * Este método resuelve el laberinto, para ir obteniendo datos en tiempo
     * real de como se resuelve el algoritmo y el camino óptimo debemos
     * subscribirnos a su listener.
     *
     * @param laberinto El laberinto que contiene la situación inicial y final
     * sobre el que se calculará el camino más corto entre dichos puntos.
     * @param sleepTime Tiempo de espera entre iteraciones del algoritmo, es
     * decir cada vez que decida una ruta se aplicara dicho sleep.
     */
    public void calcularAlgoritmo(Laberinto laberinto, long sleepTime) {
        int xInicio = laberinto.situacion.x;
        int yInicio = laberinto.situacion.y;
        while (!isFinish(laberinto)) {
            ArrayList<Nodo> adyacentes = descubrirAdyacentes(laberinto);
            Nodo mejorNodo = decidirMejorCamino(laberinto);
            laberinto.situacion.nodosDescubiertos.remove(mejorNodo);
            if (laberinto.situacion.nodosDescubiertos.isEmpty()) {
                break;
            }
            laberinto.situacion.moverSituacion(mejorNodo);
            fireIterationFinished(adyacentes, mejorNodo);
        }
        ArrayList<Nodo> camino = getCaminoOptimo(xInicio, yInicio, laberinto.situacion.caminoRecorrido);
        laberinto.situacion.setCaminoOptimo(camino);
        fireAlgorithmFinished(camino);
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException ex) {
        }
    }

    private ArrayList<Nodo> getCaminoOptimo(int xInicio, int yInicio, ArrayList<Nodo> caminoRecorrido) {
        boolean caminoEncontrado = false;
        ArrayList<Nodo> camino = new ArrayList<Nodo>();
        int numeroNodo = caminoRecorrido.size() - 1;
        while (!caminoEncontrado) {
            Nodo nodo = caminoRecorrido.get(numeroNodo);
            Nodo nodoAnterior = nodo.getNodoAnterior();
            if (nodoAnterior != null) {
                numeroNodo = getPosicionCamino(nodo.getNodoAnterior().x, nodo.getNodoAnterior().y, caminoRecorrido);
            } else {
                caminoEncontrado = true;
            }
            camino.add(nodo);
        }
        camino.remove(0);
        return camino;
    }

    private int getPosicionCamino(int x, int y, ArrayList<Nodo> caminoRecorrido) {
        int pos = 0;
        for (Nodo nodo : caminoRecorrido) {
            if (nodo.x == x && nodo.y == y) {
                pos = caminoRecorrido.indexOf(nodo);
                break;
            }
        }
        return pos;
    }

    private boolean isFinish(Laberinto laberinto) {
        final Situacion situacionActual = laberinto.situacion;
        final Point puntoFinal = laberinto.situacionFinal;

        return (situacionActual.x == puntoFinal.x && situacionActual.y == puntoFinal.y);
    }

    private Nodo decidirMejorCamino(Laberinto laberinto) {
        final Situacion situacionActual = laberinto.situacion;
        Nodo mejorNodo = situacionActual.nodosDescubiertos.get(0);
        for (Nodo nodo : situacionActual.nodosDescubiertos) {
            if (mejorNodo.getF() > nodo.getF()) {
                mejorNodo = nodo;
            }
        }
        return mejorNodo;
    }

    private boolean checkAguaDiagonal(Point origen, Point destino, Laberinto laberinto) {
        if (origen.x != destino.x && origen.y != destino.y) {
            Point p1 = new Point(destino.x, origen.y);
            Point p2 = new Point(origen.x, destino.y);
            Point[] points = new Point[2];
            points[0] = p1;
            points[1] = p2;
            for (Point point : points) {
                if (checkBounds(point, laberinto) && laberinto.isAgua(point)) {
                    return false;
                }
            }
        }
        return true;
    }

    private ArrayList<Nodo> descubrirAdyacentes(Laberinto laberinto) {
        final Situacion situacionActual = laberinto.situacion;
        final ArrayList<Nodo> nodosToAppend = new ArrayList<Nodo>();
        final ArrayList<Nodo> nodosAdyacentes = (ArrayList<Nodo>) situacionActual.nodosDescubiertos;

        final Point puntoFinal = laberinto.situacionFinal;

        final int iniX = situacionActual.x - 1;
        final int iniY = situacionActual.y - 1;
        final int xFinal = situacionActual.x + 1;
        final int yFinal = situacionActual.y + 1;


        for (int n = iniX; n <= xFinal; n++) {
            for (int m = iniY; m <= yFinal; m++) {
                Point punto = new Point(n, m);
                if ((situacionActual.x != n || situacionActual.y != m)
                        && checkBounds(punto, laberinto)
                        && !laberinto.isAgua(punto)
                        && !checkIsVisited(punto, situacionActual.caminoRecorrido)
                        && !checkIsVisitedOrBetter(punto, situacionActual.nodosDescubiertos, situacionActual)
                        && checkAguaDiagonal(new Point(situacionActual.x, situacionActual.y), punto, laberinto)) {
                    int H = heuristica.calculateHeuristica(punto, puntoFinal);
                    int coste = checkDistance(n, m, situacionActual);
                    Nodo anterior = null;
                    if (!situacionActual.caminoRecorrido.isEmpty()) {
                        anterior = situacionActual.caminoRecorrido.get(situacionActual.caminoRecorrido.size() - 1);
                    }
                    Nodo nodo = new Nodo(H, coste + situacionActual.G, n, m, anterior);
                    nodosToAppend.add(nodo);
                }
            }
        }
        nodosAdyacentes.addAll(nodosToAppend);
        return nodosToAppend;
    }

    private int checkDistance(int x, int y, Situacion situacion) {
        int coste = DIAGONAL;
        if (x == situacion.x || y == situacion.y) {
            coste = RECTO;
        }
        return coste;
    }

    private boolean checkIsVisitedOrBetter(Point punto, ArrayList<Nodo> listaNodos, Situacion situacion) {
        Nodo nodoMejor = null;
        for (Nodo nodo : listaNodos) {
            if (nodo.x == punto.x && nodo.y == punto.y) {
                if (nodo.G <= checkDistance(punto.x, punto.y, situacion) + situacion.G) {
                    return true;
                } else {
                    nodoMejor = nodo;
                    break;
                }
            }
        }
        if (nodoMejor != null) {
            listaNodos.remove(nodoMejor);
        }
        return false;
    }

    private boolean checkIsVisited(Point punto, ArrayList<Nodo> listaNodos) {
        for (Nodo nodo : listaNodos) {
            if (nodo.x == punto.x && nodo.y == punto.y) {
                return true;
            }
        }
        return false;
    }

    private boolean checkBounds(Point pointToCheck, Laberinto laberinto) {
        final int x = pointToCheck.x;
        final int y = pointToCheck.y;

        final boolean isLeft = x >= 0;
        final boolean isRigth = x < laberinto.n;
        final boolean isBot = y < laberinto.m;
        final boolean isTop = y >= 0;

        return isTop && isBot && isRigth && isLeft;
    }

    private void fireAlgorithmFinished(ArrayList<Nodo> caminoOptimo) {
        for (AStarListener aStarListener : listeners) {
            aStarListener.finishAlgorithm(caminoOptimo);
        }
    }

    /**
     *
     * @param listener Agregar un listener a esta clase.
     */
    public synchronized void addListener(AStarListener listener) {
        this.listeners.add(listener);
    }

    /**
     *
     * @param listener Borrar un listener a esta clase.
     */
    public synchronized void removeListener(AStarListener listener) {
        this.listeners.remove(listener);
    }

    private void fireIterationFinished(ArrayList<Nodo> descubiertos, Nodo mejorNodo) {
        for (AStarListener aStarListener : listeners) {
            aStarListener.finishIteration(descubiertos, mejorNodo);
        }
    }
}
